Mastering Hero Animations in Flutter
You may be asking, what should I know about Hero animations that I don’t already know?
That may be true.
In fact, Flutter makes displaying Hero animations really trivial. Wrap the widgets that you want to animate to and from with the Hero
widget, give them the same tag and you’re done.
But, what if you want to achieve the following?
After this, a normal Hero
animation looks like a little boring! Doesn’t it?
Mastering Hero Animations has two sides:
- Knowing all the possibilities that the widget offers to make astonishing UIs.
- Understanding what is happening behind the scenes; how is the flight of the widget triggered by the
NavigatorObserver
? how does the flight takes place on theNavigator
’sOverlay
? and so on.
The first one will be enough if you just try to squeeze the Hero
possibilities to apply to your app.
But if you want to go beyond and understand how all this is orchestrated you will need to go deeper into the Flutter framework.
My intention is to cover both aspects, but since the post was getting very long, I have decided to leave the architecture and internals explanation for a second part.
The topics discussed in this post will lay the ground for what will come in the next one.
(For the tests I’m using the application built in Madrid Flutter Study Jam).
Let’s structure the post around Hero properties or parameters.
Hero Properties
We will better understand Hero
properties if we look at it in slow motion (use timeDilation
for that). You literally see the widget “flying” from one page to the other.
So, how can we customise the behaviour of the Hero
animation? This is the definition of the Hero
widget:
class Hero extends StatefulWidget {
final Object tag; final HeroFlightShuttleBuilder flightShuttleBuilder;
final CreateRectTween createRectTween;
final TransitionBuilder placeholderBuilder; final Widget child;
...
}
Let’s see these properties one at a time.
final HeroFlightShuttleBuilder flightShuttleBuilder
flightShuttleBuilder
is a builder that lets you change what you see on the overlay during the flight. By default, it returns the hero widget itself but you can return whatever widget you want.
The builder takes several parameters, one of them being the animation itself. By manipulating the animation, you can do things like the following:
And this is the code:
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget;
return RotationTransition(
turns: animation,
child: toHero.child,
);
}
The transition is easy because we use the animation as it is given in the parameter list. The animation provides a liner interpolation of double values and is used as such in the RotationTransition
.
But we can tweak/control the animation to have not so boring transitions. In the following code we are fading the hero by providing a quadratic function to the animation.
flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return FadeTransition(
opacity: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: ValleyQuadraticCurve()),
),
),
),
child: toHero.child,
);
}class ValleyQuadraticCurve extends Curve {
@override
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
return 4 * math.pow(t - 0.5, 2);
}
...
Nice!
Or applied to a ScaleTransition
In this case we use a different quadratic function where we have a peak instead of a valley.
flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: PeakQuadraticCurve()),
),
),
),
child: toHero.child,
);
}class PeakQuadraticCurve extends Curve {
@override
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
return -15 * math.pow(t, 2) + 15 * t + 1;
}
...
Finally, we can combine all of these transitions to produce the first animation in the post. Rotating hero when pushing and fading hero when popping.
flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: PeakQuadraticCurve()),
),
),
),
child: flightDirection == HeroFlightDirection.push
? RotationTransition(
turns: animation,
child: toHero.child,
)
: FadeTransition(
opacity: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: ValleyQuadraticCurve()),
),
),
),
child: toHero.child,
),
);
}
final CreateRectTween createRectTween
As the definition in the doc says “Defines how the destination hero’s bounds change as it flies from the starting route to the destination route.”
CreateRectTween
is a function that returns a RectTween
and controls the alignment and position of the rectangle where the flying widget is placed during the flight. By default, the path is defined by the function MaterialRectArcTween
and the size of the rectangle is a linear interpolation between the from and to rectangles.
Normally, we would not need to change the default behaviour. The official docs change it for the radial hero animation demo.
But, what if you want to deviate from the Material guidelines and standard arc path and do something like the following?
This might seem awkward in some transitions, but the possibility is there.
You need to create your custom CreateRectTween
function returning a customRectTween
.
static RectTween _createRectTween(Rect begin, Rect end) {
return QuadraticRectTween(begin: begin, end: end);
}...
Hero(
tag: "thisistheherotag",
createRectTween: _createRectTween,
...
QuadraticRectTween
is just a copy of MaterialRectCenterArcTween
where I have replaced the call toMaterialPointArcTween
in the _initialize()
method by the following class:
class QuadraticOffsetTween extends Tween<Offset> {
QuadraticOffsetTween({
Offset begin,
Offset end,
}) : super(begin: begin, end: end);
@override
Offset lerp(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
final double x = -11 * begin.dx * math.pow(t, 2) +
(end.dx + 10 * begin.dx) * t + begin.dx;
final double y = -2 * begin.dy * math.pow(t, 2) +
(end.dy + 1 * begin.dy) * t + begin.dy;
return Offset(x, y);
}
}
It provides a path based on the quadratic functions for x
and y
center properties.
Using the CreateRectTween
you could also change the size of the rectangle, though this is something we have already achieved previously by using ScaleTransition
.
final TransitionBuilder placeholderBuilder
The last parameter to analyse is placeholderBuilder
.
As we will see in the next post, the Hero
is very basic. Its build()
method either renders the child widget when no flight is active (as any other parent widget), or the placeholder widget when the flight is active. If no placeholderBuilder
is provided, it just renders en empty SizedBox
.
In the following image you can see the child widget itself faded in its original place.
Passing a placeholderBuilder not always makes sense, as in this example, but it can provide a hint to the user that something is missing in that place when the Hero
is flying.
Hero(
tag: "thisistheherotag",
placeholderBuilder: (context, child) {
return Opacity(opacity: 0.2, child: child);
},
The builder receives the child as a parameter, but nothing prevents you of returning any other widget.
And that’s all. I hope you found the flight interesting. In the next post I will get into the internals of the Hero
flight itself.
You can follow me in my twitter profile for updates.